package io.shuttle.mbe.core

import android.annotation.SuppressLint
import android.content.Context
import android.os.Environment
import android.util.Log
import timber.log.Timber
import java.io.File
import java.time.Instant
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoUnit
import java.util.regex.Pattern

/**
 * TODO: print to js console
 */
class Logger {
    companion object {
        const val TAG = "__Wab__"

        fun tags(vararg subTags: String?): String {
            if (subTags.isNotEmpty()) {
                return "$TAG/${subTags.joinTo(StringBuilder(), "/")}"
            }
            return TAG
        }

        fun v(message: String) {
            v(TAG, message)
        }

        fun v(tag: String?, message: String) {
            log(Log.VERBOSE, tag, message, null)
        }

        fun d(message: String) {
            d(TAG, message)
        }

        fun d(tag: String?, message: String) {
            log(Log.DEBUG, tag, message, null)
        }

        fun i(message: String) {
            i(TAG, message)
        }

        fun i(tag: String?, message: String) {
            log(Log.INFO, tag, message, null)
        }

        fun w(message: String) {
            w(TAG, message)
        }

        fun w(tag: String?, message: String) {
            log(Log.WARN, tag, message, null)
        }

        fun e(message: String) {
            e(TAG, message, null)
        }


        fun e(message: String, th: Throwable?) {
            e(TAG, message, th)
        }


        fun e(tag: String?, message: String?, th: Throwable?) {
            log(Log.ERROR, tag, message, th)
        }


        fun shouldLog(): Boolean {
            return true
        }

        private fun log(
            level: Int,
            tag: String?,
            message: String?,
            th: Throwable?,
            initPrinter: Boolean = false
        ) {
            if (!shouldLog()) return
            Timber.tag(TAG).log(level, th, message, tag)
        }
    }
}

class LogFileTimberTree(
    private val logsDirectory: File,
    private val logFileNameProvider: LogFileNameProvider = DailyLogFileNameProvider()
) : Timber.Tree() {
    private val logFile: File = File(logsDirectory, getLogFileNameForNow())
    private val priorityMap = mapOf(
        2 to "VERBOSE",
        3 to "DEBUG",
        4 to "INFO",
        5 to "WARN",
        6 to "ERROR",
        7 to "ASSERT",
    )

    init {
        @SuppressLint("LogNotTimber")
        Log.d("LogFileTimberTree", "LogFile: ${logFile.absolutePath}")
    }

    companion object {
        const val MAX_TAG_LENGTH = 33
        const val CALL_STACK_INDEX = 5
    }

    override fun log(
        priority: Int,
        tag: String?,
        message: String,
        t: Throwable?
    ) {
        // FIXME: Call requires API level 26 (current min is 21)
        val time = LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)
        val logValue = priorityToString(priority)
        val error = if (t == null) "" else "\n${t}\n${t.message}\n${t.stackTraceToString()}"
        logFile.appendText(
            "${time.padEnd(23)} ${logValue.padEnd(10)} ${
                "[" + (getFixedTag(tag)).padEnd(MAX_TAG_LENGTH) + "]"
            } $message $error"
        )
    }

    private fun createStackElementTag(element: StackTraceElement): String {
        var tag = element.className
        val m = Pattern.compile("(\\$\\d+)+$").matcher(tag)
        if (m.find()) {
            tag = m.replaceAll("")
        }
        tag = tag.substring(tag.lastIndexOf('.') + 1)
        return if (tag.length <= MAX_TAG_LENGTH) {
            tag
        } else tag.substring(0, MAX_TAG_LENGTH)
    }

    private fun getFixedTag(tag: String?): String {
        if (tag != null) {
            return tag
        }
        // DO NOT switch this to Thread.getCurrentThread().getStackTrace(). The test will pass
        // because Robolectric runs them on the JVM but on Android the elements are different.
        val stackTrace = Throwable().stackTrace
        check(stackTrace.size > CALL_STACK_INDEX) {
            "Synthetic stacktrace didn't have enough elements: are you using proguard?"
        }
        return createStackElementTag(stackTrace[CALL_STACK_INDEX + 1])
    }

    private fun priorityToString(priority: Int) =
        priorityMap.getOrElse(priority) { throw IllegalAccessException() }

    private fun getLogFileNameForNow() = logFileNameProvider.getFileName(LocalDateTime.now())

}

interface LogStorageProvider {
    fun getStorageDirectory(logDirName: String = "app-logs"): File
    fun File.prepareDirectory() = apply {
        if (!this.exists()) {
            this.mkdirs()
        }
    }
}

class ExternalLogStorageProvider(private val context: Context) : LogStorageProvider {
    override fun getStorageDirectory(logDirName: String): File {
        return File(
            context.getExternalFilesDir(null) ?: Environment.getExternalStorageDirectory(),
            logDirName
        ).prepareDirectory()
    }
}

class InternalLogStorageProvider(private val context: Context) : LogStorageProvider {
    override fun getStorageDirectory(logDirName: String): File {
        return File(context.filesDir, logDirName).prepareDirectory()
    }
}

fun File.cleanLogFiles(theDay: Long) {
    var minBefore = Instant.now().minus(theDay, ChronoUnit.DAYS)
    (listFiles()?.toList() ?: listOf())
        .filter { it.isFile }
        .filter { Instant.ofEpochMilli(it.lastModified()).isBefore(minBefore) }
        .forEach { it.delete() }
}

abstract class LogFileNameProvider(private val prefix: String, private val extension: String) {
    fun getFileName(dateTime: LocalDateTime) = "$prefix${getFileTag(dateTime)}.$extension"
    abstract fun getFileTag(dateTime: LocalDateTime): String

    companion object {
        const val DEFAULT_PREFIX = "app_logs-"
        const val DEFAULT_EXTENSION = "log"
    }
}

class ConstantDateTimeLogFileNameProvider(
    private val dateTime: Long = System.currentTimeMillis(),
    private val dateTimeFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMdd_HHmm"),
    prefix: String = DEFAULT_PREFIX,
    extension: String = DEFAULT_EXTENSION
) : LogFileNameProvider(prefix, extension) {
    override fun getFileTag(dateTime: LocalDateTime): String = dateTimeFormatter.format(dateTime)
}

class DailyLogFileNameProvider(
    private val dateTimeFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMdd_HHmm"),
    prefix: String = DEFAULT_PREFIX,
    extension: String = DEFAULT_EXTENSION
) : LogFileNameProvider(prefix, extension) {
    override fun getFileTag(dateTime: LocalDateTime): String = dateTimeFormatter.format(dateTime)
}